﻿using System.Collections.ObjectModel;
using InSimDotNet;
using InSimDotNet.Packets;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace LFSRecord2.Model.Replay
{
    public delegate void ReplayCheckCompletedEventHandler(object sender, ReplayCheckCompletedEventArgs e);
    public delegate void ReplayRecordingCompletedEventHandler(object sender, ReplayRecordingCompletedEventArgs e);
    public delegate void ReplayRecordingErrorEventHandler(object sender, ReplayRecordingErrorEventArgs e);

    public class ImportRpdFromLfs : IDisposable
    {
        public enum CurrentActions
        {
            Idle,
            CheckingReplay,
            RecordPrepare,
            Recording,
        }

        CurrentActions _currentAction = CurrentActions.Idle;

        /// <summary>
        /// Reference to our insim connection.
        /// </summary>
        private InSim _insim;

        /// <summary>
        /// Contains information about how to record from the replay, and which replay. 
        /// It's a collection because there can be more than one replay in the project and we need to have information about all of them,
        /// so we can present the user will all appropriate recording options.
        /// </summary>
        public Collection<ImportRpdSectionInfo> RInfo { get; private set; }

        /// <summary>
        /// Internally used to track which replay we are currently checking / recording from
        /// </summary>
        private int _currentRInfoIndex = 0;

        /// <summary>
        /// Contains the replay data after recording. This is not a collection; we will only record from one replay at a time.
        /// </summary>
        public ReplayData RData { get; private set; }

        public ImportRpdFromLfs(InSim insim)
        {
            _insim = insim;
            RInfo = new Collection<ImportRpdSectionInfo>();
            RData = new ReplayData();
        }

        /// <summary>
        /// Start the replay checking process. 
        /// It will use the data from RInfo, which you should populate before calling this method. Otherwise this method has nothing to check.
        /// </summary>
        public void CheckReplay()
        {
            if (RInfo.Count == 0)
            {
                OnReplayCheckComplete(new ReplayCheckCompletedEventArgs(RInfo));
                return;
            }

            // Get first valid replay index from RInfo
            _currentRInfoIndex = -1;
            _currentRInfoIndex = nextValidRInfoIndex();

            // No valid replays?
            if (_currentRInfoIndex == -1)
            {
                OnReplayRecordingError(new ReplayRecordingErrorEventArgs(ReplayError.RIP_OK, "No valid replays in project. Check replay filename extensions."));
                return;
            }

            _currentAction = CurrentActions.CheckingReplay;

            // Assign rip listener
            _insim.Bind<IS_RIP>(onReplayConfirm);

            // Instruct LFS to seek to start time of the first (valid) replay in RInfo
            if ((LfsRecordController.LfsState.Flags & StateFlags.ISS_SHIFTU) > 0)
                _insim.Send(new IS_SCH { CharB = (char)27 });
            _insim.Send("/entry");
            Thread.Sleep(300);
            setReplay(_currentRInfoIndex, TimeSpan.Zero);
        }

        private int nextValidRInfoIndex()
        {
            for (int a = 0; a < RInfo.Count; a++)
            {
                if (RInfo[a].ReplayType != ReplayTypes.NoReplay && a > _currentRInfoIndex)
                {
                    return a;
                }
            }

            return -1;
        }

        /// <summary>
        /// Seek in a replay
        /// </summary>
        /// <param name="rInfoIndex">The RInfo index of the replay we want to seek in</param>
        /// <param name="cTime">The CTime in hundredths of seconds</param>
        private void setReplay(int rInfoIndex, TimeSpan cTime)
        {
            Debug.WriteLine("Sending rip to LFS " + RInfo[rInfoIndex].ReplayName);
            _insim.Send(new IS_RIP
            {
                ReqI = 1,
                MPR = (RInfo[rInfoIndex].ReplayType == ReplayTypes.Mpr) ? ReplayMode.Mpr : ReplayMode.Spr,
                Paused = true,
                CTime = cTime,
                RName = Path.GetFileNameWithoutExtension(RInfo[rInfoIndex].ReplayName)
            });
        }

        private void onReplayConfirm(InSim insim, IS_RIP rip)
        {
            //Debug.WriteLine("got is_rip");

            switch (rip.Error)
            {
                case ReplayError.RIP_CORRUPTED:
                case ReplayError.RIP_DEDICATED:
                case ReplayError.RIP_DEST_OOB:
                case ReplayError.RIP_NOT_FOUND:
                case ReplayError.RIP_NOT_REPLAY:
                case ReplayError.RIP_OOS:
                case ReplayError.RIP_UNKNOWN:
                case ReplayError.RIP_UNLOADABLE:
                case ReplayError.RIP_USER:
                case ReplayError.RIP_WRONG_MODE:
                    Debug.WriteLine("Replay ERROR : {0}", rip.Error);
                    if (_currentAction != CurrentActions.CheckingReplay)
                    {
                        Cancel();
                        OnReplayRecordingError(new ReplayRecordingErrorEventArgs(rip.Error, "Replay error"));
                    }
                    break;

                default:
                    if (_currentAction == CurrentActions.CheckingReplay)
                    {
                        Debug.WriteLine("Replay OK {0} - replay length : {1}", rip.Error, rip.TTime);
                        RInfo[_currentRInfoIndex].ReplayLength = rip.TTime.TotalSeconds;
                    }
                    else if (_currentAction == CurrentActions.RecordPrepare)
                    {
                        // Replay record start time has been reached.
                        // Now we must first get the status of the race at this point (which players are in the race)

                        // Listen for the IS_STA on our special function
                        _insim.Bind<IS_STA>(CollectNumPlayers);

                        // Request a IS_STA so we can read out the player number at this point in the replay
                        _insim.Send(TinyType.TINY_SST, 1);
                    }
                    else if (_currentAction == CurrentActions.Recording)
                    {
                        if (RInfo[_currentRInfoIndex].CurStepSeekTime < RInfo[_currentRInfoIndex].RecordEndTime)
                        {
                            // We must seek to the next step.
                            RInfo[_currentRInfoIndex].CurStepSeekTime += 1;

                            // Verify seek time does not exceed end time
                            if (RInfo[_currentRInfoIndex].CurStepSeekTime > RInfo[_currentRInfoIndex].RecordEndTime)
                                RInfo[_currentRInfoIndex].CurStepSeekTime = RInfo[_currentRInfoIndex].RecordEndTime;

                            Thread.Sleep(100);

                            // Seek in replay
                            setReplay(_currentRInfoIndex, TimeSpan.FromSeconds(RInfo[_currentRInfoIndex].CurStepSeekTime));

                            // Adjust progress
                            double totalTime = RInfo[_currentRInfoIndex].RecordEndTime - RInfo[_currentRInfoIndex].RecordStartTime;
                            OnReplayRecordingProgress(new ReplayRecordingProgessEventArgs((RInfo[_currentRInfoIndex].CurStepSeekTime - RInfo[_currentRInfoIndex].RecordStartTime) / totalTime));
                        }
                        else
                        {
                            Debug.WriteLine("Reached recording end time");

                            // Sleep a bit to make sure delayed packets are still recorded
                            //Thread.Sleep(500);

                            // We are done with recording, unbind packet listener
                            _insim.PacketReceived -= new EventHandler<PacketEventArgs>(insim_PacketReceived);
                            // Stop receiving mci
                            _insim.Send(SmallType.SMALL_NLI, 0);

                            // Sign off
                            RData.PostProcess();
                            OnReplayRecordingCompleted(new ReplayRecordingCompletedEventArgs(RData));
                            _currentAction = CurrentActions.Idle;
                        }
                    }
                    break;
            }

            if (_currentAction == CurrentActions.CheckingReplay)
            {
                RInfo[_currentRInfoIndex].ReplayError = rip.Error;

                _currentRInfoIndex = nextValidRInfoIndex();

                // If this was the last replay in RInfo, finalise the checking process
                if (_currentRInfoIndex == -1)
                {
                    _insim.Unbind<IS_RIP>(onReplayConfirm);
                    _currentAction = CurrentActions.Idle;
                    OnReplayCheckComplete(new ReplayCheckCompletedEventArgs(RInfo));
                }
                else
                {
                    // Get info of next replay
                    if ((LfsRecordController.LfsState.Flags & StateFlags.ISS_SHIFTU) > 0)
                        _insim.Send(new IS_SCH { CharB = (char)27 });
                    _insim.Send("/entry");
                    Thread.Sleep(300);
                    setReplay(_currentRInfoIndex, TimeSpan.Zero);
                }
            }
        }
        
        /// <summary>
        /// Starts the recording process of a replay.
        /// </summary>
        /// <param name="replayIndex">The replay index in RInfo</param>
        public void RecordReplayData(int replayIndex)
        {
            if (replayIndex > RInfo.Count - 1)
            {
                OnReplayRecordingError(new ReplayRecordingErrorEventArgs(ReplayError.RIP_NOT_FOUND, "Invalid replay index given."));
                return;
            }
            if (RInfo[replayIndex].ReplayType == ReplayTypes.NoReplay)
            {
                OnReplayRecordingError(new ReplayRecordingErrorEventArgs(ReplayError.RIP_NOT_REPLAY, "Given replay name does not end in .spr or .mpr"));
                return;
            }

            _currentAction = CurrentActions.RecordPrepare;
            _currentRInfoIndex = replayIndex;

            // Add insim listeners
            _insim.Bind<IS_RIP>(onReplayConfirm);
            //_insim.PacketReceived += new EventHandler<PacketEventArgs>(insim_PacketReceived);

            // First we seek to the recording start time. In CollectPlayer we'll continue the recording process.
            setReplay(_currentRInfoIndex, TimeSpan.FromSeconds(RInfo[_currentRInfoIndex].RecordStartTime));
        }

        private void CollectNumPlayers(InSim insim, IS_STA sta)
        {
            if (_currentAction != CurrentActions.RecordPrepare)
                return;

            Debug.WriteLine("Received STA for numplayers : {0}", sta.NumP);
            RInfo[_currentRInfoIndex].NumPlayers = sta.NumP;

            _insim.Unbind<IS_STA>(CollectNumPlayers);
            _insim.Bind<IS_NPL>(CollectPlayer);

            // Request all IS_NPL packets
            _insim.Send(TinyType.TINY_NPL, 1);
        }

        private void CollectPlayer(InSim insim, IS_NPL npl)
        {
            Debug.WriteLine("Received NPL : {0} ({1})", npl.PName, npl.PLID);
            // Add missing players (all that come in here, basically)
            if (RData.GetPlayerByID(npl.PLID) == null)
            {
                RData.Players.Add(new ReplayPlayerData(npl.PLID, StringHelper.StripColors(npl.PName)));
            }

            // Have all players?
            if (RInfo[_currentRInfoIndex].NumPlayers == RData.Players.Count)
            {
                Debug.WriteLine("Finished retrieving all initial players ({0})", RInfo[_currentRInfoIndex].NumPlayers);
                //FinishRetrieveAllPlayersFromReplay();
                _insim.Unbind<IS_NPL>(CollectPlayer);

                _currentAction = CurrentActions.Recording;

                // Setup RData object
                RData.ReplayName = RInfo[_currentRInfoIndex].ReplayName;
                RData.ReplayLength = RInfo[_currentRInfoIndex].ReplayLength;
                RData.RecordStartTime = RInfo[_currentRInfoIndex].RecordStartTime;
                RData.RecordEndTime = RInfo[_currentRInfoIndex].RecordEndTime;
                RData.CurRecordTime = RInfo[_currentRInfoIndex].RecordStartTime;

                // Switch on MCI receiving, at 20 or 25pps (depending on insimversion)
                if (LfsRecordController.LfsState.InSimVersion <= 4)
                {
                    _insim.Send(SmallType.SMALL_NLI, 50);
                    Debug.WriteLine("recording with InSimVersion 4");
                }
                else
                {
                    _insim.Send(SmallType.SMALL_NLI, 40);
                    Debug.WriteLine("recording with InSimVersion 5");
                }
                
                // Add generic packet listener
                _insim.PacketReceived += new EventHandler<PacketEventArgs>(insim_PacketReceived);

                // First retrieve mci packets of _current_ position
                _insim.Send(TinyType.TINY_MCI, 1);

                // MCI precision recording (realtime)?
                if (RInfo[_currentRInfoIndex].ReplayType == ReplayTypes.Mpr)
                {
                    RInfo[_currentRInfoIndex].CurRecordTime = RInfo[_currentRInfoIndex].RecordStartTime;
                    // Here we just un-pause the replay and record all MCI until endtime is reached.
                    LfsRecordController.PauseLFS(false);
                }
                else
                {
                    // Set the recording seek time in CurSeekTime
                    //if (RInfo[_currentRInfoIndex].ReplayType == ReplayTypes.Spr)
                    //{
                    //    // With spr, we can simply seek straight to the recording end time
                    //    RInfo[_currentRInfoIndex].CurStepSeekTime = RInfo[_currentRInfoIndex].RecordEndTime;
                    //}
                    //else
                    //{
                        // We must seek towards the recording end time in steps (2 seconds atm).
                        RInfo[_currentRInfoIndex].CurStepSeekTime = RInfo[_currentRInfoIndex].RecordStartTime + 2;
                    //}

                    // Verify seek time does not exceed end time
                    if (RInfo[_currentRInfoIndex].CurStepSeekTime > RInfo[_currentRInfoIndex].RecordEndTime)
                        RInfo[_currentRInfoIndex].CurStepSeekTime = RInfo[_currentRInfoIndex].RecordEndTime;

                    // Seek in replay
                    setReplay(_currentRInfoIndex, TimeSpan.FromSeconds(RInfo[_currentRInfoIndex].CurStepSeekTime));
                }

                return;
            }
        }

        private void insim_PacketReceived(object sender, PacketEventArgs e)
        {
            int plid = -1;
            ReplayPlayerData pData;

            // Filter out a few packets that we don't need
            switch (e.Packet.Type)
            {
                case PacketType.ISP_NPL:
                    IS_NPL npl = (IS_NPL)e.Packet;
                    plid = npl.PLID;

                    if (RData.GetPlayerByID(plid) == null)
                    {
                        RData.Players.Add(new ReplayPlayerData(plid, StringHelper.StripColors(npl.PName)));
                    }
                    break;

                case PacketType.ISP_PLL:
                    IS_PLL pll = (IS_PLL)e.Packet;
                    plid = pll.PLID;
                    break;

                case PacketType.ISP_MCI:
                    IS_MCI mci = (IS_MCI)e.Packet;
                    CompCarCustom ccc;
                    bool lastPlayer = false;
                    foreach (CompCar cc in mci.Info)
                    {
                        ccc = new CompCarCustom(cc);
                        pData = RData.GetPlayerByID(ccc.PLID);
                        if (pData != null)
                        {
                            pData.DataNodes.Add(new ReplayDataNode() 
                            { 
                                ReplayTime = RData.CurRecordTime, 
                                Packet = null, 
                                MciInfo = ccc 
                            });
                        }

                        if ((ccc.Info & CompCarFlags.CCI_LAST) > 0)
                        {
                            lastPlayer = true;
                        }
                    }

                    // In case of precision MPR recording, process progress bar and check if we reached the end
                    if (RInfo[_currentRInfoIndex].ReplayType == ReplayTypes.Mpr)
                    {
                        if (lastPlayer)
                        {
                            RInfo[_currentRInfoIndex].CurRecordTime += (LfsRecordController.LfsState.InSimVersion <= 4) ? 0.05 : 0.04;
                        }

                        // Reached end?
                        if (RInfo[_currentRInfoIndex].CurRecordTime >= RInfo[_currentRInfoIndex].RecordEndTime)
                        {
                            _insim.PacketReceived -= new EventHandler<PacketEventArgs>(insim_PacketReceived);
                            // Stop receiving mci
                            _insim.Send(SmallType.SMALL_NLI, 0);

                            // Sign off
                            RData.PostProcess();
                            OnReplayRecordingCompleted(new ReplayRecordingCompletedEventArgs(RData));
                            _currentAction = CurrentActions.Idle;

                            // Pause LFS
                            //LfsRecordController.PauseLFS(true);
                        }
                        else
                        {
                            // Adjust progress
                            double totalTime = RInfo[_currentRInfoIndex].RecordEndTime - RInfo[_currentRInfoIndex].RecordStartTime;
                            OnReplayRecordingProgress(new ReplayRecordingProgessEventArgs((RInfo[_currentRInfoIndex].CurRecordTime - RInfo[_currentRInfoIndex].RecordStartTime) / totalTime));
                        }
                    }

                    return;

                default:
                    return;
            }

            // Store the packet in RData
            Debug.WriteLine("Recording insim packet : {0}", e.Packet.Type);
        }

        public event ReplayCheckCompletedEventHandler ReplayCheckCompleted;

        public virtual void OnReplayCheckComplete(ReplayCheckCompletedEventArgs e)
        {
            ReplayCheckCompletedEventHandler temp = ReplayCheckCompleted;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        public event EventHandler<ReplayRecordingProgessEventArgs> ReplayRecordingProgress;

        public virtual void OnReplayRecordingProgress(ReplayRecordingProgessEventArgs e)
        {
            EventHandler<ReplayRecordingProgessEventArgs> temp = ReplayRecordingProgress;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        public event ReplayRecordingCompletedEventHandler ReplayRecordingCompleted;

        public virtual void OnReplayRecordingCompleted(ReplayRecordingCompletedEventArgs e)
        {
            ReplayRecordingCompletedEventHandler temp = ReplayRecordingCompleted;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        public event ReplayRecordingErrorEventHandler ReplayRecordingError;

        public virtual void OnReplayRecordingError(ReplayRecordingErrorEventArgs e)
        {
            ReplayRecordingErrorEventHandler temp = ReplayRecordingError;
            if (temp != null)
            {
                temp(this, e);
            }
        }

        /// <summary>
        /// Cancel the recording process (at any time). Must be called if we do not complete the whole recording process naturally.
        /// </summary>
        public void Cancel()
        {
            _currentAction = CurrentActions.Idle;
            if (_insim != null)
            {
                _insim.Send(SmallType.SMALL_NLI, 0);
                _insim.Unbind<IS_RIP>(onReplayConfirm);
                //_insim.Unbind<IS_STA>(CollectNumPlayers); 
                _insim.PacketReceived -= new EventHandler<PacketEventArgs>(insim_PacketReceived);
                LfsRecordController.PauseLFS(true);
            }
            Dispose();
        }

        public void Dispose()
        {
            this.Dispose(true);

            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            _insim = null;
            RInfo = null;
            //RData = null;
            ReplayCheckCompleted = null;
            ReplayRecordingCompleted = null;
            ReplayRecordingError = null;
        }
    }
}
